// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('bmm', function() { /** @const */ var Tree = cr.ui.Tree; /** @const */ var TreeItem = cr.ui.TreeItem; var treeLookup = {}; var tree; // Manager for persisting the expanded state. var expandedManager = { /** * A map of the collapsed IDs. * @type {Object} */ map: 'bookmarkTreeState' in localStorage ? JSON.parse(localStorage['bookmarkTreeState']) : {}, /** * Set the collapsed state for an ID. * @param {string} The bookmark ID of the tree item that was expanded or * collapsed. * @param {boolean} expanded Whether the tree item was expanded. */ set: function(id, expanded) { if (expanded) delete this.map[id]; else this.map[id] = 1; this.save(); }, /** * @param {string} id The bookmark ID. * @return {boolean} Whether the tree item should be expanded. */ get: function(id) { return !(id in this.map); }, /** * Callback for the expand and collapse events from the tree. * @param {!Event} e The collapse or expand event. */ handleEvent: function(e) { this.set(e.target.bookmarkId, e.type == 'expand'); }, /** * Cleans up old bookmark IDs. */ cleanUp: function() { for (var id in this.map) { // If the id is no longer in the treeLookup the bookmark no longer // exists. if (!(id in treeLookup)) delete this.map[id]; } this.save(); }, timer: null, /** * Saves the expanded state to the localStorage. */ save: function() { clearTimeout(this.timer); var map = this.map; // Save in a timeout so that we can coalesce multiple changes. this.timer = setTimeout(function() { localStorage['bookmarkTreeState'] = JSON.stringify(map); }, 100); } }; // Clean up once per session but wait until things settle down a bit. setTimeout(expandedManager.cleanUp.bind(expandedManager), 1e4); /** * Creates a new tree item for a bookmark node. * @param {!Object} bookmarkNode The bookmark node. * @constructor * @extends {TreeItem} */ function BookmarkTreeItem(bookmarkNode) { var ti = new TreeItem({ label: bookmarkNode.title, bookmarkNode: bookmarkNode, // Bookmark toolbar and Other bookmarks are not draggable. draggable: bookmarkNode.parentId != ROOT_ID }); ti.__proto__ = BookmarkTreeItem.prototype; treeLookup[bookmarkNode.id] = ti; return ti; } BookmarkTreeItem.prototype = { __proto__: TreeItem.prototype, /** @override */ remove: function(child) { TreeItem.prototype.remove.call(this, child); if (child.bookmarkNode) delete treeLookup[child.bookmarkNode.id]; }, /** * The ID of the bookmark this tree item represents. * @type {string} */ get bookmarkId() { return this.bookmarkNode.id; } }; /** * Asynchronousy adds a tree item at the correct index based on the bookmark * backend. * * Since the bookmark tree only contains folders the index we get from certain * callbacks is not very useful so we therefore have this async call which * gets the children of the parent and adds the tree item at the desired * index. * * This also exoands the parent so that newly added children are revealed. * * @param {!cr.ui.TreeItem} parent The parent tree item. * @param {!cr.ui.TreeItem} treeItem The tree item to add. * @param {Function=} f A function which gets called after the item has been * added at the right index. */ function addTreeItem(parent, treeItem, opt_f) { chrome.bookmarks.getChildren(parent.bookmarkNode.id, function(children) { var index = children.filter(bmm.isFolder).map(function(item) { return item.id; }).indexOf(treeItem.bookmarkNode.id); parent.addAt(treeItem, index); parent.expanded = true; if (opt_f) opt_f(); }); } /** * Creates a new bookmark list. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {HTMLButtonElement} */ var BookmarkTree = cr.ui.define('tree'); BookmarkTree.prototype = { __proto__: Tree.prototype, decorate: function() { Tree.prototype.decorate.call(this); this.addEventListener('expand', expandedManager); this.addEventListener('collapse', expandedManager); bmm.tree = this; }, handleBookmarkChanged: function(id, changeInfo) { var treeItem = treeLookup[id]; if (treeItem) treeItem.label = treeItem.bookmarkNode.title = changeInfo.title; }, handleChildrenReordered: function(id, reorderInfo) { var parentItem = treeLookup[id]; // The tree only contains folders. var dirIds = reorderInfo.childIds.filter(function(id) { return id in treeLookup; }).forEach(function(id, i) { parentItem.addAt(treeLookup[id], i); }); }, handleCreated: function(id, bookmarkNode) { if (bmm.isFolder(bookmarkNode)) { var parentItem = treeLookup[bookmarkNode.parentId]; var newItem = new BookmarkTreeItem(bookmarkNode); addTreeItem(parentItem, newItem); } }, handleMoved: function(id, moveInfo) { var treeItem = treeLookup[id]; if (treeItem) { var oldParentItem = treeLookup[moveInfo.oldParentId]; oldParentItem.remove(treeItem); var newParentItem = treeLookup[moveInfo.parentId]; // The tree only shows folders so the index is not the index we want. We // therefore get the children need to adjust the index. addTreeItem(newParentItem, treeItem); } }, handleRemoved: function(id, removeInfo) { var parentItem = treeLookup[removeInfo.parentId]; var itemToRemove = treeLookup[id]; if (parentItem && itemToRemove) parentItem.remove(itemToRemove); }, insertSubtree: function(folder) { if (!bmm.isFolder(folder)) return; var children = folder.children; this.handleCreated(folder.id, folder); for (var i = 0; i < children.length; i++) { var child = children[i]; this.insertSubtree(child); } }, /** * Returns the bookmark node with the given ID. The tree only maintains * folder nodes. * @param {string} id The ID of the node to find. * @return {BookmarkTreeNode} The bookmark tree node or null if not found. */ getBookmarkNodeById: function(id) { var treeItem = treeLookup[id]; if (treeItem) return treeItem.bookmarkNode; return null; }, /** * Fetches the bookmark items and builds the tree control. */ reload: function() { /** * Recursive helper function that adds all the directories to the * parentTreeItem. * @param {!cr.ui.Tree|!cr.ui.TreeItem} parentTreeItem The parent tree * element to append to. * @param {!Array.} bookmarkNodes A list of bookmark * nodes to be added. * @return {boolean} Whether any directories where added. */ function buildTreeItems(parentTreeItem, bookmarkNodes) { var hasDirectories = false; for (var i = 0, bookmarkNode; bookmarkNode = bookmarkNodes[i]; i++) { if (bmm.isFolder(bookmarkNode)) { hasDirectories = true; var item = new BookmarkTreeItem(bookmarkNode); parentTreeItem.add(item); var anyChildren = buildTreeItems(item, bookmarkNode.children); item.expanded = anyChildren && expandedManager.get(bookmarkNode.id); } } return hasDirectories; } var self = this; chrome.bookmarkManagerPrivate.getSubtree('', true, function(root) { self.clear(); buildTreeItems(self, root[0].children); cr.dispatchSimpleEvent(self, 'load'); }); }, /** * Clears the tree. */ clear: function() { // Remove all fields without recreating the object since other code // references it. for (var id in treeLookup) { delete treeLookup[id]; } this.textContent = ''; }, /** @override */ remove: function(child) { Tree.prototype.remove.call(this, child); if (child.bookmarkNode) delete treeLookup[child.bookmarkNode.id]; } }; return { BookmarkTree: BookmarkTree, BookmarkTreeItem: BookmarkTreeItem, treeLookup: treeLookup, tree: tree }; });